/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jena.sparql.serializer;

import org.apache.jena.atlas.io.IndentedWriter;
import org.apache.jena.graph.Triple;
import org.apache.jena.shared.PrefixMapping;
import org.apache.jena.sparql.ARQInternalErrorException;
import org.apache.jena.sparql.algebra.OpAsQuery;
import org.apache.jena.sparql.core.Var;
import org.apache.jena.sparql.expr.*;
import org.apache.jena.sparql.syntax.Element;
import org.apache.jena.sparql.util.FmtUtils;

/** Output expressions in SPARQL syntax */

public class FmtExprSPARQL {
    static final int INDENT = 2;

    FmtExprARQVisitor visitor;

    public FmtExprSPARQL(IndentedWriter writer, SerializationContext cxt) {
        visitor = new FmtExprARQVisitor(writer, cxt);
    }

    // Top level writing of an expression.
    public void format(Expr expr) {
        expr.visit(visitor);
    }

    public static void format(IndentedWriter out, Expr expr) {
        format(out, expr, null);
    }

    public static void format(IndentedWriter out, Expr expr, SerializationContext cxt) {
        FmtExprSPARQL fmt = new FmtExprSPARQL(out, cxt);
        fmt.format(expr);
    }

    private static class FmtExprARQVisitor implements ExprVisitor {
        IndentedWriter out;
        SerializationContext context;

        public FmtExprARQVisitor(IndentedWriter writer, PrefixMapping pmap) {
            this(writer, new SerializationContext(pmap));
        }

        public FmtExprARQVisitor(IndentedWriter writer, SerializationContext cxt) {
            out = writer;
            context = cxt;
            if ( context == null )
                context = new SerializationContext();
        }

        @Override
        public void visit(ExprFunction0 expr) {
            if ( expr.getOpName() == null ) {
                printInFunctionForm(expr);
                return;
            }
            out.print("( ");
            out.print(expr.getOpName());
            out.print(" ");
        }

        @Override
        public void visit(ExprFunction1 expr) {
            if ( expr.getOpName() == null ) {
                printInFunctionForm(expr);
                return;
            }
            out.print("( ");
            out.print(expr.getOpName());
            out.print(" ");
            expr.getArg().visit(this);
            out.print(" )");
        }

        @Override
        public void visit(ExprFunction2 expr) {
            if ( expr.getOpName() == null ) {
                printInFunctionForm(expr);
                return;
            }
            out.print("( ");
            expr.getArg1().visit(this);
            out.print(" ");
            out.print(expr.getOpName());
            out.print(" ");
            expr.getArg2().visit(this);
            out.print(" )");
        }

        @Override
        public void visit(ExprFunction3 expr) {
            printInFunctionForm(expr);
        }

        @Override
        public void visit(ExprFunctionN func) {
            if ( func instanceof E_OneOf ) {
                E_OneOf oneOf = (E_OneOf)func;
                out.print("( ");
                oneOf.getLHS().visit(this);
                out.print(" IN ");
                printExprList(oneOf.getRHS());
                out.print(" )");
                return;
            }

            if ( func instanceof E_NotOneOf ) {
                E_NotOneOf oneOf = (E_NotOneOf)func;
                out.print("( ");
                oneOf.getLHS().visit(this);
                out.print(" NOT IN ");
                printExprList(oneOf.getRHS());
                out.print(" )");
                return;
            }
            printInFunctionForm(func);
        }

        private void printInFunctionForm(ExprFunction func) {
            out.print(func.getFunctionPrintName(context));
            printExprList(func.getArgs());
        }

        private void printExprList(Iterable<Expr> exprs) {
            out.print("(");
            boolean first = true;
            for ( Expr expr : exprs ) {
                if ( expr == null )
                    break;
                if ( !first )
                    out.print(", ");
                first = false;
                expr.visit(this);
            }
            out.print(")");
        }

        @Override
        public void visit(ExprFunctionOp funcOp) {
            String fn = funcOp.getFunctionPrintName(context);
            if ( funcOp instanceof E_NotExists )
                fn = "NOT EXISTS";
            else if ( funcOp instanceof E_Exists )
                fn = "EXISTS";
            else
                throw new ARQInternalErrorException("Unrecognized ExprFunctionOp: " + fn);

            FormatterElement fmtElt = new FormatterElement(out, context);
            out.print(fn);
            out.print(" ");
            int indent = out.getAbsoluteIndent();
            int currentCol = out.getCol();
            try {
                out.setAbsoluteIndent(currentCol);
                Element el = funcOp.getElement();
                if ( el == null )
                    el = OpAsQuery.asQuery(funcOp.getGraphPattern()).getQueryPattern();
                el.visit(fmtElt);
            }
            finally {
                out.setAbsoluteIndent(indent);
            }
        }

        @Override
        public void visit(NodeValue nv) {
            out.print(nv.asQuotedString(context));
        }

        @Override
        public void visit(ExprNone none) {
            out.print("NONE");
        }

        @Override
        public void visit(ExprVar nv) {
            String s = nv.getVarName();
            if ( Var.isBlankNodeVarName(s) ) {
                // Return to a bNode via the bNode mapping of a variable.
                Var v = Var.alloc(s);
                out.print(context.getBNodeMap().asString(v));
            } else {
                // Print in variable form or as an aggregator expression
                out.print(nv.asSparqlExpr());
            }
        }

        @Override
        public void visit(ExprTripleTerm tripleTerm) {
            Triple t = tripleTerm.getTriple();
            out.print(FmtUtils.stringForNode(tripleTerm.getNode(), context));
        }

        @Override
        public void visit(ExprAggregator eAgg) {
            out.print(eAgg.asSparqlExpr(context));
        }
    }
}
